From a324df95e62bc843c7b2dff84592e86dceb0d6fb Mon Sep 17 00:00:00 2001 From: Guillaume Louel <37544189+glouel@users.noreply.github.com> Date: Mon, 13 Nov 2023 17:22:50 +0100 Subject: [PATCH] 3.3.5 CompanionBridge Fix display detection for enabling/disabling individual screens --- Aerial.xcodeproj/project.pbxproj | 16 +++-- Aerial/Source/Models/Aerial.swift | 60 ++++++++++++++++ Aerial/Source/Models/CompanionBridge.swift | 68 ++++++++++++++++++ .../Models/Hardware/DisplayDetection.swift | 3 +- .../Source/Models/Hardware/NightShift.swift | 12 ++++ Aerial/Source/Models/Locations.swift | 16 +++++ Aerial/Source/Views/AerialView.swift | 69 +++++++++++++++---- Aerial/Source/Views/Layers/MessageLayer.swift | 8 ++- .../Views/Layers/Weather/WeatherLayer.swift | 5 +- 9 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 Aerial/Source/Models/CompanionBridge.swift diff --git a/Aerial.xcodeproj/project.pbxproj b/Aerial.xcodeproj/project.pbxproj index a444661d..9bd077ab 100644 --- a/Aerial.xcodeproj/project.pbxproj +++ b/Aerial.xcodeproj/project.pbxproj @@ -830,6 +830,9 @@ F06FCF1A28DB8673007558BA /* tl.json in Resources */ = {isa = PBXBuildFile; fileRef = F06FCF1928DB8673007558BA /* tl.json */; }; F06FCF1B28DB8673007558BA /* tl.json in Resources */ = {isa = PBXBuildFile; fileRef = F06FCF1928DB8673007558BA /* tl.json */; }; F06FCF1C28DB8673007558BA /* tl.json in Resources */ = {isa = PBXBuildFile; fileRef = F06FCF1928DB8673007558BA /* tl.json */; }; + F07221872AD4354E001F5452 /* CompanionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07221862AD4354E001F5452 /* CompanionBridge.swift */; }; + F07221882AD4354E001F5452 /* CompanionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07221862AD4354E001F5452 /* CompanionBridge.swift */; }; + F07221892AD4354E001F5452 /* CompanionBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = F07221862AD4354E001F5452 /* CompanionBridge.swift */; }; F0A3E0A32884683D005E8D8D /* CompanionCacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A3E0A12884683D005E8D8D /* CompanionCacheViewController.swift */; }; F0A3E0A42884683D005E8D8D /* CompanionCacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A3E0A12884683D005E8D8D /* CompanionCacheViewController.swift */; }; F0A3E0A52884683D005E8D8D /* CompanionCacheViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0A3E0A12884683D005E8D8D /* CompanionCacheViewController.swift */; }; @@ -1183,6 +1186,7 @@ F008DAFC23AADCFB00739DE1 /* Brightness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Brightness.swift; sourceTree = ""; }; F05E805328AE8A9C0088B9C5 /* NowPlayingCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingCollectionView.swift; sourceTree = ""; }; F06FCF1928DB8673007558BA /* tl.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tl.json; sourceTree = ""; }; + F07221862AD4354E001F5452 /* CompanionBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanionBridge.swift; sourceTree = ""; }; F0A3E0A12884683D005E8D8D /* CompanionCacheViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompanionCacheViewController.swift; sourceTree = ""; }; F0A3E0A22884683D005E8D8D /* CompanionCacheViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CompanionCacheViewController.xib; sourceTree = ""; }; FA143CD61BDA3E880041A82B /* AerialApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AerialApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1774,6 +1778,7 @@ 03AD45FE22981B0C00261325 /* CustomVideoFolders+helpers.swift */, 03C97BBF24B60E2F00739CED /* FileHelpers.swift */, 03FF192F269709AB00A0FA7F /* PlaybackSpeed.swift */, + F07221862AD4354E001F5452 /* CompanionBridge.swift */, ); path = Models; sourceTree = ""; @@ -2528,6 +2533,7 @@ buildActionMask = 2147483647; files = ( 0396D50B24B8B7ED00CC021B /* DisplayDetection.swift in Sources */, + F07221882AD4354E001F5452 /* CompanionBridge.swift in Sources */, 0396D50C24B8B7ED00CC021B /* LayerOffsets.swift in Sources */, 0396D50D24B8B7ED00CC021B /* APISecrets.swift in Sources */, 0396D50E24B8B7ED00CC021B /* NightShift.swift in Sources */, @@ -2670,6 +2676,7 @@ buildActionMask = 2147483647; files = ( 03B8742424E41CF8008E3D1B /* CacheSetupViewController.swift in Sources */, + F07221892AD4354E001F5452 /* CompanionBridge.swift in Sources */, 03D3A11524C5FC770091FE99 /* AspectFillNSImageView.swift in Sources */, 031FB7A2248A873C0054BAFD /* PrefsCache.swift in Sources */, F00864B523AAE7F0003210EF /* DarkMode.swift in Sources */, @@ -2821,6 +2828,7 @@ buildActionMask = 2147483647; files = ( 03D1E78A2284471A00D10CF7 /* DisplayDetection.swift in Sources */, + F07221872AD4354E001F5452 /* CompanionBridge.swift in Sources */, 0306336B23A142FA00046A59 /* LayerOffsets.swift in Sources */, 0385FC59242B9AE1007E6513 /* APISecrets.swift in Sources */, F00864B723AAE8E9003210EF /* NightShift.swift in Sources */, @@ -3241,7 +3249,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3.3.3; + CURRENT_PROJECT_VERSION = 3.3.5; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; @@ -3249,7 +3257,7 @@ INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 3.3.3; + MARKETING_VERSION = 3.3.5; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3270,7 +3278,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 3.3.3; + CURRENT_PROJECT_VERSION = 3.3.5; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 3L54M5L5KK; ENABLE_HARDENED_RUNTIME = YES; @@ -3278,7 +3286,7 @@ INSTALL_PATH = "$(HOME)/Library/Screen Savers"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 3.3.3; + MARKETING_VERSION = 3.3.5; OTHER_CODE_SIGN_FLAGS = "--timestamp"; PRODUCT_BUNDLE_IDENTIFIER = com.johncoates.Aerial; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Aerial/Source/Models/Aerial.swift b/Aerial/Source/Models/Aerial.swift index 5170401e..8571316e 100644 --- a/Aerial/Source/Models/Aerial.swift +++ b/Aerial/Source/Models/Aerial.swift @@ -251,6 +251,66 @@ class Aerial: NSObject { return (output, task.terminationStatus) } + + func shell(_ command:String, args: [String] = []) -> String + { + let task = Process() + var arguments = ["-c"] + arguments.append(command) + arguments += args + task.launchPath = "/bin/bash" + task.arguments = arguments + + let pipe = Pipe() + task.standardOutput = pipe + task.launch() + + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + task.waitUntilExit() + + return output ?? "" + +/* let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: String.Encoding.utf8)! +*/ /*if output.count > 0 { + //remove newline character. + let lastIndex = output.index(before: output.endIndex) + return String(output[output.startIndex ..< lastIndex]) + }*/ + //return output + } + + // Launch a process through shell and capture/return output + func shell(executableURL: String, arguments: [String] = []) -> (String?, Int32) { + let task = Process() + task.executableURL = URL(fileURLWithPath: executableURL) + task.arguments = arguments + + let pipe = Pipe() + task.standardOutput = pipe + task.standardError = pipe + + if #available(OSX 10.13, *) { + do { + try task.run() + } catch { + // handle errors + debugLog("Error: \(error.localizedDescription)") + } + } else { + // A non existing command will crash 10.12 + task.launch() + } + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + let output = String(data: data, encoding: .utf8) + task.waitUntilExit() + + return (output, task.terminationStatus) + } + /* func trySettings() { diff --git a/Aerial/Source/Models/CompanionBridge.swift b/Aerial/Source/Models/CompanionBridge.swift new file mode 100644 index 00000000..5af5bdec --- /dev/null +++ b/Aerial/Source/Models/CompanionBridge.swift @@ -0,0 +1,68 @@ +// +// CompanionBridge.swift +// Aerial +// +// Created by Guillaume Louel on 09/10/2023. +// Copyright © 2023 Guillaume Louel. All rights reserved. +// +// This acts as our bridge to Companion when the plugin needs data FROM companion +// Currently using DistributedNotificationCenter, until *that* breaks too... + +import Foundation + +struct CompanionBridge { + static var nightShiftSunrise: Date? + static var nightShiftSunset: Date? + + static var locationLat: Double? + static var locationLong: Double? + + static func setNotifications() { + debugLog("🌉 seting up CompanionBridge") + + // Get nightshift + DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name("com.glouel.aerial.nightshift"), object: nil, queue: nil) { notification in + debugLog("🌉😻 received nightshift") + debugLog(notification.debugDescription) + + if let sunrise = notification.userInfo?["sunrise"] as? Date { + debugLog("parsed sunrise") + nightShiftSunrise = sunrise + } else { + debugLog("can't parse sunrise") + } + + if let sunset = notification.userInfo?["sunset"] as? Date { + debugLog("parsed sunset") + nightShiftSunset = sunset + } + } + + // Get location + DistributedNotificationCenter.default().addObserver(forName: NSNotification.Name("com.glouel.aerial.location"), object: nil, queue: nil) { notification in + debugLog("🌉😻 received location") + debugLog(notification.debugDescription) + + if let lat = notification.userInfo?["latitude"] as? Double { + debugLog("parsed latitude") + locationLat = lat + } else { + debugLog("can't parse latitude") + } + + if let long = notification.userInfo?["longitude"] as? Double { + debugLog("parsed longitude") + locationLong = long + } + } + + + // Test request + DistributedNotificationCenter.default().postNotificationName(NSNotification.Name("com.glouel.aerial.getnightshift"), object: nil, deliverImmediately: true) + + if PrefsInfo.weather.locationMode == .useCurrent || PrefsTime.timeMode == .locationService { + debugLog("🌉 asking for location") + DistributedNotificationCenter.default().postNotificationName(NSNotification.Name("com.glouel.aerial.getlocation"), object: nil, deliverImmediately: true) + } + } +} diff --git a/Aerial/Source/Models/Hardware/DisplayDetection.swift b/Aerial/Source/Models/Hardware/DisplayDetection.swift index 6a0d2d14..d65c5976 100644 --- a/Aerial/Source/Models/Hardware/DisplayDetection.swift +++ b/Aerial/Source/Models/Hardware/DisplayDetection.swift @@ -357,7 +357,8 @@ final class DisplayDetection: NSObject { func isScreenActive(id: CGDirectDisplayID) -> Bool { let screen = findScreenWith(id: id) - + debugLog("ISA : \(screen)") + switch PrefsDisplays.displayMode { case .allDisplays: // This one is easy diff --git a/Aerial/Source/Models/Hardware/NightShift.swift b/Aerial/Source/Models/Hardware/NightShift.swift index a33a6661..55476c0c 100644 --- a/Aerial/Source/Models/Hardware/NightShift.swift +++ b/Aerial/Source/Models/Hardware/NightShift.swift @@ -39,6 +39,17 @@ struct NightShift { // swiftlint:disable:next large_tuple static func getInformation() -> (Bool, sunrise: Date?, sunset: Date?, error: String?) { + // Sonoma workaround + if !Aerial.helper.underCompanion { + if #available(macOS 14.0, *) { + if CompanionBridge.nightShiftSunrise != nil { + debugLog("Nightshift using CompanionBridge data") + return (true, CompanionBridge.nightShiftSunrise, CompanionBridge.nightShiftSunset, nil) + } else { + return (false, nil, nil, "Sonoma requires Aerial Companion") + } + } + } if isNightShiftDataCached { return (nightShiftAvailable, nightShiftSunrise, nightShiftSunset, nil) } @@ -50,6 +61,7 @@ struct NightShift { } let (nsInfo, ts) = Aerial.helper.shell(launchPath: cbdpath, arguments: ["nightshift-internal"]) + if ts != 0 { // Task didn't return correctly ? Abort diff --git a/Aerial/Source/Models/Locations.swift b/Aerial/Source/Models/Locations.swift index 03cf0a58..860922de 100644 --- a/Aerial/Source/Models/Locations.swift +++ b/Aerial/Source/Models/Locations.swift @@ -25,6 +25,22 @@ class Locations: NSObject { func getCoordinates(failure: @escaping (_ error: String) -> Void, success: @escaping (_ response: CLLocationCoordinate2D) -> Void) { + // Sonoma workaround via CompanionBridge + if !Aerial.helper.underCompanion { + if #available(macOS 14.0, *) { + if CompanionBridge.locationLat != nil && CompanionBridge.locationLong != nil { + debugLog("Location using CompanionBridge data") + + let coords = CLLocationCoordinate2DMake( + CompanionBridge.locationLat! as CLLocationDegrees, + CompanionBridge.locationLong! as CLLocationDegrees) + + success(coords) + return + } + } + } + // Perhaps they are cached already ? if coordinates != nil { debugLog("Location using cached data") diff --git a/Aerial/Source/Views/AerialView.swift b/Aerial/Source/Views/AerialView.swift index adce3641..9a0508ce 100644 --- a/Aerial/Source/Views/AerialView.swift +++ b/Aerial/Source/Views/AerialView.swift @@ -124,6 +124,13 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { // Clear log if > 1MB on startup rollLogIfNeeded() + // Set Companion bridge notifications under Sonoma, but not under Companion + if !Aerial.helper.underCompanion { + if #available(macOS 14, *) { + CompanionBridge.setNotifications() + } + } + // legacyScreenSaver always return true for isPreview on Catalina // We need to detect and override ourselves // This is finally fixed in Ventura @@ -134,7 +141,6 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { if frame.width < 400 && frame.height < 300 { preview = true } - // This is where we manage our location info layers, clock, etc self.layerManager = LayerManager(isPreview: preview) @@ -149,7 +155,14 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { } else { // We need to delay things under Sonoma because legacyScreenSaver is awesome if #available(macOS 14.0, *) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + var delay = 0.01 + + // If nightshift we delay more + if !Aerial.helper.underCompanion && PrefsTime.timeMode == .nightShift { + delay = 0.5 + } + + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { debugLog("🖼️ AVinit delayed setup!") self.setup() } @@ -168,6 +181,14 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { // Clear log if > 1MB on startup rollLogIfNeeded() + // Set Companion bridge notifications under Sonoma, but not under Companion + if !Aerial.helper.underCompanion { + if #available(macOS 14, *) { + CompanionBridge.setNotifications() + } + } + + self.layerManager = LayerManager(isPreview: false) // ... @@ -228,6 +249,9 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { } } + + + // First we check the system appearance, as it relies on our view Aerial.helper.computeDarkMode(view: self) @@ -280,7 +304,17 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { var thisScreen: Screen? = nil if #available(macOS 14.0, *) { - //thisScreen = displayDetection.alternateFindScreenWith(frame: self.frame, backingScaleFactor: self.window?.backingScaleFactor ?? 1) + if foundScreen == nil { + debugLog("🖼️ missing foundScreen, workarounding \(String(describing: self.window?.screen))") + if let missingScreen = self.window?.screen { + debugLog("🖼️ screen attached") + matchScreen(thisScreen: missingScreen) + } else { + errorLog("🖼️ still missing screen") + } + } else { + debugLog("🖼️ early foundScreen ok \(String(describing: foundScreen))") + } } else { thisScreen = displayDetection.findScreenWith(frame: self.frame) } @@ -301,12 +335,14 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { // Is the current screen disabled by user ? if !isPreview { // If it's an unknown screen, we leave it enabled - if let screen = thisScreen { + if let screen = foundScreen { if !displayDetection.isScreenActive(id: screen.id) { // Then we disable and exit debugLog("🖼️ This display is not active, disabling") isDisabled = true return + } else { + debugLog("Screen is active") } } } else { @@ -380,18 +416,25 @@ final class AerialView: ScreenSaverView, CAAnimationDelegate { debugLog(self.window?.screen.debugDescription ?? "Unknown") if let thisScreen = self.window?.screen { - let screenID = thisScreen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! CGDirectDisplayID - - debugLog(screenID.description) - - foundScreen = DisplayDetection.sharedInstance.findScreenWith(id: screenID) - foundFrame = foundScreen?.bottomLeftFrame - - debugLog("🖼️🌾 Using : \(String(describing: foundScreen))") - debugLog("🥬🌾 window.screen \(String(describing: self.window?.screen.debugDescription))") + matchScreen(thisScreen: thisScreen) + } else { + // For some reason we may not have a screen here! + debugLog("🖼️ no screen attached, will try again later") } } + func matchScreen(thisScreen: NSScreen) { + let screenID = thisScreen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! CGDirectDisplayID + + debugLog(screenID.description) + + foundScreen = DisplayDetection.sharedInstance.findScreenWith(id: screenID) + foundFrame = foundScreen?.bottomLeftFrame + + debugLog("🖼️🌾 Using : \(String(describing: foundScreen))") + debugLog("🥬🌾 window.screen \(String(describing: self.window?.screen.debugDescription))") + } + // Handle window resize override func viewDidEndLiveResize() { layerManager.redrawAllCorners() diff --git a/Aerial/Source/Views/Layers/MessageLayer.swift b/Aerial/Source/Views/Layers/MessageLayer.swift index 36b13964..0ebd460b 100644 --- a/Aerial/Source/Views/Layers/MessageLayer.swift +++ b/Aerial/Source/Views/Layers/MessageLayer.swift @@ -121,7 +121,13 @@ class MessageLayer: AnimationTextLayer { if config.shellScript != "" { if FileManager.default.fileExists(atPath: PrefsInfo.message.shellScript) { - let (result, _) = Aerial.helper.shell(launchPath: PrefsInfo.message.shellScript) + var result: String? + + if #available(macOS 14.0, *) { + (result, _) = Aerial.helper.shell(executableURL: PrefsInfo.message.shellScript) + } else { + (result, _) = Aerial.helper.shell(launchPath: PrefsInfo.message.shellScript) + } debugLog("result " + (result ?? "")) if let res = result { diff --git a/Aerial/Source/Views/Layers/Weather/WeatherLayer.swift b/Aerial/Source/Views/Layers/Weather/WeatherLayer.swift index 4e113020..cc6eb859 100644 --- a/Aerial/Source/Views/Layers/Weather/WeatherLayer.swift +++ b/Aerial/Source/Views/Layers/Weather/WeatherLayer.swift @@ -142,9 +142,10 @@ class WeatherLayer: AnimationLayer { if PrefsInfo.weather.mode == .current { if cachedWeather != nil { + debugLog("weather using cache") displayWeatherBlock() } else { - print("fetching") + debugLog("fetching fresh weather") OpenWeather.fetch { result in switch result { case .success(let openWeather): @@ -157,8 +158,10 @@ class WeatherLayer: AnimationLayer { } } else { if cachedForecast != nil && cachedWeather != nil { + debugLog("weather using cache") displayWeatherBlock() } else { + debugLog("fetching fresh weather") Forecast.fetch { result in switch result { case .success(let openWeather):