Skip to content
Closed
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
1 change: 1 addition & 0 deletions Crisp/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
}
EdgeCrossingRow()
DisconnectBuiltinRow()
BatchDisplayConnectionView()
ExpandableRowStateful(icon: "display.2", iconActive: false,
label: "Virtual Displays", state: state, key: \.showVirtualDisplays)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "ExternalDisplayConnection.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion Crisp/Models/DDCOperationQueuePool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ final class DDCOperationQueuePool: @unchecked Sendable {
/// Parks every per-display queue, present and future, until the returned closure
/// runs; `onIdle` fires once queued work drains, so the caller knows I2C is free.
/// Each queue gives up after `timeout` so a lost release cannot wedge DDC.
func hold(timeout: TimeInterval, onIdle: @escaping () -> Void) -> () -> Void {
func hold(timeout: TimeInterval, onIdle: @escaping () -> Void) -> @Sendable () -> Void {
let hold = Hold(timeout: timeout)
lock.lock()
activeHolds.append(hold)
Expand Down
21 changes: 21 additions & 0 deletions Crisp/Models/DisplayConfigurationGate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Foundation

/// Unlike the UI timeout, this lease ends only when the blocking transaction finishes.
final class DisplayConfigurationGate: @unchecked Sendable {
private let lock = NSLock()
private var pending = false

var isPending: Bool { lock.withLock { pending } }

func begin() -> Bool {
lock.withLock {
guard !pending else { return false }
pending = true
return true
}
}

func finish() {
lock.withLock { pending = false }
}
}
36 changes: 36 additions & 0 deletions Crisp/Models/DisplayConnectionPlan.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
struct DisplayConnectionPlan: Equatable {
enum Action: Equatable {
case toggleAll
case reconnectAll
case disconnectAll
}

struct Display {
let uuid: String
var isBuiltin = false
var isMain = false
var isVirtual = false
var isActive = true
var name = ""
var isAvailable = true
}

let reconnectUUIDs: [String]
let disconnectUUIDs: [String]

static func make(action: Action, connected: [Display], disconnected: [String], builtinUUIDs: Set<String> = []) -> Self {
var seen: Set<String> = []
let held = disconnected.filter { seen.insert($0).inserted }
if action == .reconnectAll || (action == .toggleAll && !held.isEmpty) {
let builtins = builtinUUIDs.union(connected.filter(\.isBuiltin).map(\.uuid))
return Self(reconnectUUIDs: held.filter { !builtins.contains($0) }, disconnectUUIDs: [])
}
seen = Set(held)
let physical = connected.filter { !$0.isVirtual && seen.insert($0.uuid).inserted }
let active = physical.filter(\.isActive)
guard let keeper = active.first(where: { $0.isBuiltin }) ?? active.first(where: { $0.isMain }) ?? active.first else {
return Self(reconnectUUIDs: [], disconnectUUIDs: [])
}
return Self(reconnectUUIDs: [], disconnectUUIDs: physical.filter { !$0.isBuiltin && $0.uuid != keeper.uuid }.map(\.uuid))
}
}
19 changes: 19 additions & 0 deletions Crisp/Models/DisplayConnectionQueue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@MainActor
final class DisplayConnectionQueue {
private var tail: Task<Void, Never>?
private var generation: UInt64 = 0

func run<Output: Sendable>(_ operation: @escaping @MainActor () async -> Output) async -> Output {
let previous = tail
let task = Task { @MainActor in
await previous?.value
return await operation()
}
generation &+= 1
let requestGeneration = generation
tail = Task { @MainActor in _ = await task.value }
let result = await task.value
if generation == requestGeneration { tail = nil }
return result
}
}
39 changes: 39 additions & 0 deletions Crisp/Models/ExternalDisplayConnectionState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
struct ExternalDisplayConnectionState {
struct Entry: Identifiable, Equatable {
let id: String
let name: String
let isConnected: Bool
let isAvailable: Bool
let isProtected: Bool
}

let entries: [Entry]
var columnCount: Int { min(4, max(1, entries.count)) }
var rowCount: Int { (entries.count + columnCount - 1) / columnCount }
var hasProtectedDisplay: Bool { entries.contains { $0.isProtected } }
var isOn: Bool { entries.contains { $0.isConnected && !$0.isProtected } }
var canReconnect: Bool { entries.contains { !$0.isConnected && $0.isAvailable } }
var canDisconnect: Bool { entries.contains { $0.isConnected && !$0.isProtected } }

static func make(connected: [DisplayConnectionPlan.Display], disconnected: [DisplayConnectionPlan.Display],
canRestoreBuiltin: Bool = false) -> Self {
let held = Set(disconnected.map(\.uuid))
let physical = connected.filter { !$0.isVirtual && !held.contains($0.uuid) }
let active = physical.filter(\.isActive)
let keeper = active.first(where: { $0.isBuiltin }) ?? active.first(where: { $0.isMain }) ?? active.first
let protectedUUID = keeper?.isBuiltin == false && !canRestoreBuiltin ? keeper?.uuid : nil
var seen: Set<String> = []
let external = (physical + disconnected).filter {
!$0.isBuiltin && !$0.isVirtual && seen.insert($0.uuid).inserted
}.sorted { $0.uuid < $1.uuid }
// UUID ordering keeps names in place when WindowServer changes the online list.
let counts = Dictionary(grouping: external, by: \.name).mapValues(\.count)
var occurrence: [String: Int] = [:]
return Self(entries: external.map { display in
occurrence[display.name, default: 0] += 1
let suffix = counts[display.name, default: 0] > 1 ? " (\(occurrence[display.name, default: 1]))" : ""
return Entry(id: display.uuid, name: display.name + suffix, isConnected: !held.contains(display.uuid),
isAvailable: display.isAvailable, isProtected: display.uuid == protectedUUID)
})
}
}
Loading