import Foundation
|
|
public struct QuickSwitchWindowItem: Equatable, Sendable {
|
public let window: AlignerWindow
|
public let primarySpaceID: UInt64?
|
|
public init(window: AlignerWindow, primarySpaceID: UInt64?) {
|
self.window = window
|
self.primarySpaceID = primarySpaceID
|
}
|
}
|
|
public struct QuickSwitchAppGroup: Equatable, Sendable {
|
public let app: AlignerApp
|
public let windows: [QuickSwitchWindowItem]
|
|
public init(app: AlignerApp, windows: [QuickSwitchWindowItem]) {
|
self.app = app
|
self.windows = windows
|
}
|
}
|
|
public struct QuickSwitchSnapshot: Equatable, Sendable {
|
public let displays: [AlignerDisplay]
|
public let appGroups: [QuickSwitchAppGroup]
|
|
public init(displays: [AlignerDisplay], appGroups: [QuickSwitchAppGroup]) {
|
self.displays = displays
|
self.appGroups = appGroups
|
}
|
}
|
|
public enum QuickSwitchSnapshotBuilder {
|
public static func snapshot(
|
displays: [AlignerDisplay],
|
windows: [AlignerWindow]
|
) -> QuickSwitchSnapshot {
|
let displayOrder = spaceOrder(from: displays)
|
let candidateWindows = windows.filter(WindowDisplayPolicy.shouldDisplay)
|
let groupedWindows = Dictionary(grouping: candidateWindows) {
|
AppIdentity(app: $0.app)
|
}
|
|
let appGroups = groupedWindows.values.compactMap { windows -> QuickSwitchAppGroup? in
|
guard let firstWindow = windows.first else { return nil }
|
let sortedWindows = windows
|
.sorted { lhs, rhs in
|
compareWindows(lhs, rhs, displayOrder: displayOrder)
|
}
|
.map { window in
|
QuickSwitchWindowItem(
|
window: window,
|
primarySpaceID: primarySpaceID(for: window, displayOrder: displayOrder)
|
)
|
}
|
|
return QuickSwitchAppGroup(app: firstWindow.app, windows: sortedWindows)
|
}
|
.sorted { lhs, rhs in
|
compareApps(lhs.app, rhs.app)
|
}
|
|
return QuickSwitchSnapshot(displays: displays, appGroups: appGroups)
|
}
|
|
private static func spaceOrder(from displays: [AlignerDisplay]) -> [UInt64: Int] {
|
var order: [UInt64: Int] = [:]
|
var nextIndex = 0
|
|
for display in displays.sorted(by: { $0.uuid.localizedStandardCompare($1.uuid) == .orderedAscending }) {
|
for space in display.spaces.sorted(by: { $0.index < $1.index }) {
|
order[space.id] = nextIndex
|
nextIndex += 1
|
}
|
}
|
|
return order
|
}
|
|
private static func compareApps(_ lhs: AlignerApp, _ rhs: AlignerApp) -> Bool {
|
let nameOrder = lhs.name.localizedCaseInsensitiveCompare(rhs.name)
|
if nameOrder != .orderedSame {
|
return nameOrder == .orderedAscending
|
}
|
|
let bundleOrder = normalized(lhs.bundleIdentifier)
|
.localizedCaseInsensitiveCompare(normalized(rhs.bundleIdentifier))
|
if bundleOrder != .orderedSame {
|
return bundleOrder == .orderedAscending
|
}
|
|
let pathOrder = normalized(lhs.executablePath)
|
.localizedCaseInsensitiveCompare(normalized(rhs.executablePath))
|
if pathOrder != .orderedSame {
|
return pathOrder == .orderedAscending
|
}
|
|
return (lhs.processIdentifier ?? Int32.max) < (rhs.processIdentifier ?? Int32.max)
|
}
|
|
private static func compareWindows(
|
_ lhs: AlignerWindow,
|
_ rhs: AlignerWindow,
|
displayOrder: [UInt64: Int]
|
) -> Bool {
|
let lhsSpaceOrder = primarySpaceOrder(for: lhs, displayOrder: displayOrder)
|
let rhsSpaceOrder = primarySpaceOrder(for: rhs, displayOrder: displayOrder)
|
if lhsSpaceOrder != rhsSpaceOrder {
|
return lhsSpaceOrder < rhsSpaceOrder
|
}
|
|
let titleOrder = lhs.title.localizedCaseInsensitiveCompare(rhs.title)
|
if titleOrder != .orderedSame {
|
return titleOrder == .orderedAscending
|
}
|
|
return lhs.id < rhs.id
|
}
|
|
private static func primarySpaceID(
|
for window: AlignerWindow,
|
displayOrder: [UInt64: Int]
|
) -> UInt64? {
|
window.spaceIDs.min { lhs, rhs in
|
(displayOrder[lhs] ?? Int.max) < (displayOrder[rhs] ?? Int.max)
|
}
|
}
|
|
private static func primarySpaceOrder(
|
for window: AlignerWindow,
|
displayOrder: [UInt64: Int]
|
) -> Int {
|
guard let primarySpaceID = primarySpaceID(for: window, displayOrder: displayOrder) else {
|
return Int.max
|
}
|
|
return displayOrder[primarySpaceID] ?? Int.max
|
}
|
|
private static func normalized(_ value: String?) -> String {
|
guard let value, !value.isEmpty else { return "\u{10FFFF}" }
|
return value
|
}
|
}
|
|
private struct AppIdentity: Hashable {
|
let bundleIdentifier: String?
|
let executablePath: String?
|
let processIdentifier: Int32?
|
let fallbackName: String
|
|
init(app: AlignerApp) {
|
self.bundleIdentifier = app.bundleIdentifier.isEmpty ? nil : app.bundleIdentifier
|
self.executablePath = app.executablePath?.isEmpty == false ? app.executablePath : nil
|
self.processIdentifier = app.processIdentifier
|
self.fallbackName = app.name
|
}
|
}
|